/***************************************************************************
 * All Rights Reserved.
 *
 * The contents of this file are subject to the Mozilla Public
 * License Version 1.1 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of
 * the License at http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS
 * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * rights and limitations under the License.
 ***************************************************************************/
package org.gbif.metadata.webapp.action;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

import javax.servlet.ServletContext;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.struts2.util.ServletContextAware;
import org.appfuse.webapp.action.BaseAction;
import org.gbif.metadata.eml.Address;
import org.gbif.metadata.eml.BoundingCoordinates;
import org.gbif.metadata.eml.Coverage;
import org.gbif.metadata.eml.Creator;
import org.gbif.metadata.eml.Dataset;
import org.gbif.metadata.eml.DatasetGPolygon;
import org.gbif.metadata.eml.Eml;
import org.gbif.metadata.eml.GeographicCoverage;
import org.gbif.metadata.eml.IntellectualRights;
import org.gbif.metadata.eml.Keyword;
import org.gbif.metadata.eml.KeywordSet;
import org.gbif.metadata.eml.Maintenance;
import org.gbif.metadata.eml.MaintenanceDescription;
import org.gbif.metadata.eml.Phone;
import org.gbif.metadata.eml.Purpose;
import org.gbif.metadata.eml.TaxonomicClassification;
import org.gbif.metadata.eml.TaxonomicCoverage;
import org.gbif.metadata.eml.TemporalCoverage;
import org.gbif.metadata.io.MetadataFileUtils;
import org.jibx.runtime.BindingDirectory;
import org.jibx.runtime.IBindingFactory;
import org.jibx.runtime.IMarshallingContext;
import org.jibx.runtime.IUnmarshallingContext;

import sun.reflect.ReflectionFactory.GetReflectionFactoryAction;

import com.opensymphony.xwork2.ModelDriven;

/**
 * Action that handles the creation and modification of EML documents
 * 
 * @author davejmartin
 */
public class MetadataEditorAction extends BaseAction implements
		ModelDriven<Eml>, ServletContextAware {

	private static final long serialVersionUID = -1539488206246518866L;

	protected Eml eml = null;

	protected Map<String, String> isoLanguageI18nCodeMap = null;

	protected List<String> majorTaxonRanks = null;

	protected List<String> otherTaxonRanks = null;

	protected List<String> creatorRoles = null;

	protected List<String> classification = null;

	protected File currentDoc = null;

	protected ServletContext context;

	protected String defaultDatasetLanguage = "en";

	protected Date now = new Date();

	protected boolean singleDateEnabled = false;
	
	protected String back;

	protected final String BACK = "back";

	protected String currentFileSessionAttribute = "currentDoc";

	protected String metadataDirectoryPath = MetadataFileUtils.metadataDirectoryPath;
	
	/**
	 * Adds current document to the request.
	 * 
	 * @see com.opensymphony.xwork2.ModelDriven#getModel()
	 */
	public Eml getModel() {
		// none supplied so check the session
		eml = (Eml) getWorkingDocument();
		if (eml == null) {
			log.debug("No EML document stored in session, creating new...");
			setupNewDoc();
		}
		return eml;
	}

	/**
	 * Blows away any changes to the cache eml model
	 */
	@Override
	public String cancel() {

		this.currentDoc = (File) getRequest().getSession().getAttribute(
				currentFileSessionAttribute);
		if (this.currentDoc != null) {
			try {
				// reinitialise the current EML object model with this
				IBindingFactory bfact = BindingDirectory.getFactory(Eml.class);
				IUnmarshallingContext uctx = bfact.createUnmarshallingContext();
				Eml eml = (Eml) uctx.unmarshalDocument(new FileInputStream(
						this.currentDoc), null);
				setWorkingDocument(eml);
				return super.cancel();
			} catch (Exception e) {
				log.error(e.getMessage(), e);
			}
		}
		return ERROR;
	}

	/**
	 * Back workflow request.
	 * 
	 * @return
	 */
	public String back() {
		return BACK;
	}

	/**
	 * Initialise a new Eml document.
	 */
	private void setupNewDoc() {
		eml = new Eml();
		eml.setDataset(new Dataset());
		eml.getDataset().setCreator(new Creator());
		eml.getDataset().getCreator().setAddress(new Address());
		eml.getDataset().setIntellectualRights(new IntellectualRights());
		eml.getDataset().setCoverage(new Coverage());
		setWorkingDocument(eml);
	}

	/**
	 * Set the current working document.
	 * 
	 * @param eml
	 */
	private void setWorkingDocument(Eml eml) {
		getRequest().getSession().setAttribute("eml", eml);
	}

	/**
	 * Place holder method - so that validation isnt applied to newly created
	 * document.
	 * 
	 * @return
	 */
	public String initEml() {
		return SUCCESS;
	}

	/**
	 * Clean out session.
	 * 
	 * @return
	 */
	public String closeDocument() {
		getRequest().getSession().removeAttribute("currentDoc");
		getRequest().getSession().removeAttribute("eml");
		return SUCCESS;
	}

	/**
	 * Clean out session.
	 * 
	 * @return
	 */
	public String createNew() {
		setupNewDoc();
		return SUCCESS;
	}

	/**
	 * Initialise dataset fields.
	 * 
	 * @return
	 */
	public String initDataset() {
		log.debug("Initialising dataset");

		Dataset dataset = eml.getDataset();
		Eml sessionEml = getWorkingDocument();

		String[] keywords = getRequest().getParameterValues("keyword");
		for (String keywordAsString : keywords) {
			if (StringUtils.isNotBlank(keywordAsString)) {
				KeywordSet keywordSet = new KeywordSet();
				Keyword keyword = new Keyword();
				keyword.setBase(keywordAsString);
				keywordSet.setKeyword(keyword);
				dataset.addKeywordSet(keywordSet);
			}
		}
		sessionEml.setDataset(dataset);
		return SUCCESS;
	}

	/**
	 * Initialise the creator element
	 * 
	 * @return
	 */
	public String initCreator() {
		log.debug("Initialising creator");
		Creator creator = eml.getDataset().getCreator();
		String phoneAsString = getRequest().getParameter("creator.phone");
		String faxAsString = getRequest().getParameter("creator.fax");
		
		//FIXME not pretty code, but restricted by the code JIBX constructs
		// with xsd2java
		Phone voice = null;
		Phone fax = null;
		if(creator.sizePhoneList()!=2){
			voice = new Phone();
			voice.setPhonetype("voice");
			creator.addPhone(voice);
			fax = new Phone();
			fax.setPhonetype("fax");
			creator.addPhone(fax);		
		} else {
			voice = creator.getPhone(0);
			fax = creator.getPhone(1);
		}
		fax.setBase(faxAsString);
		voice.setBase(phoneAsString);
		
		Eml sessionEml = getWorkingDocument();
		sessionEml.getDataset().setCreator(creator);
		return SUCCESS;
	}

	/**
	 * Pull the current document from the session or a context.
	 * 
	 * @return
	 */
	private Eml getWorkingDocument() {
		Eml sessionEml = (Eml) getRequest().getSession().getAttribute("eml");
		return sessionEml;
	}

	/**
	 * Initialise intellectual rights.
	 * 
	 * @return
	 */
	public String initIntellectualRights() {
		log.debug("Initialising intellectual rights");
		IntellectualRights intellectualRights = eml.getDataset()
				.getIntellectualRights();
		Eml sessionEml = getWorkingDocument();
		sessionEml.getDataset().setIntellectualRights(intellectualRights);
		return SUCCESS;
	}

	/**
	 * Initialise the coverage for this dataset.
	 * 
	 * Some decoding of request param into TaxonomicClassification elements
	 * here.
	 * 
	 * @return
	 */
	public String initCoverage() {
		log.debug("Initialising coverage");
		boolean isValid = true;
		// check description, gRing or boundingCoordinates supplied
		Coverage coverage = eml.getDataset().getCoverage();

		// isValid = isValid && validateGeographicCoverage(coverage);
		isValid = isValid && validateTaxonomicCoverage(coverage);
		isValid = isValid && validateTemporalCoverage(coverage);

		if (!isValid) {
			return INPUT;
		}

		Eml sessionEml = getWorkingDocument();
		Coverage sessionCoverage = sessionEml.getDataset().getCoverage();
		sessionCoverage.setTaxonomicCoverage(coverage.getTaxonomicCoverage());
		sessionCoverage.setTemporalCoverage(coverage.getTemporalCoverage());
		return SUCCESS;
	}

	/**
	 * Initialise the Geographic Coverage.
	 * 
	 * @return
	 */
	public String initGeographicCoverage() {
		log.debug("Initialising geographic coverage");
		boolean isValid = true;
		// check description, gRing or boundingCoordinates supplied
		Coverage coverage = eml.getDataset().getCoverage();
		isValid = isValid && validateGeographicCoverage(coverage);
		if (!isValid) {
			return INPUT;
		}

		Eml sessionEml = getWorkingDocument();
		Coverage sessionCoverage = sessionEml.getDataset().getCoverage();
		sessionCoverage.setGeographicCoverage(coverage.getGeographicCoverage());
		return SUCCESS;
	}

	/**
	 * Check that either a single date or a date range is supplied.
	 * 
	 * @param coverage
	 * @return
	 */
	private boolean validateTemporalCoverage(Coverage coverage) {

		TemporalCoverage tc = coverage.getTemporalCoverage();
		boolean temporalSupplied = false;
		
		if (singleDateEnabled){
			tc.setRangeOfDates(null);
			if(tc.getSingleDateTime() != null
				&& tc.getSingleDateTime().getCalendarDate() != null){
				temporalSupplied = true;
			}
		} else {

			if (tc.getRangeOfDates() != null
					&& tc.getRangeOfDates().getBeginDate() != null
					&& tc.getRangeOfDates().getBeginDate().getCalendarDate() != null
					&& tc.getRangeOfDates().getEndDate() != null
					&& tc.getRangeOfDates().getEndDate().getCalendarDate() != null) {
	
				if (tc.getRangeOfDates().getBeginDate().getCalendarDate().before(
						tc.getRangeOfDates().getEndDate().getCalendarDate())) {
					addActionError(getText("eml.dataset.coverage.temporalCoverage.rangeOfDates.bad.order"));
				}
	
				temporalSupplied = true;
				tc.setSingleDateTime(null);
			}
		}

		if (!temporalSupplied) {
			addActionError(getText("eml.dataset.coverage.temporalCoverage.mandatory"));
		}
		return temporalSupplied;
	}

	/**
	 * Validate Taxonomic Coverage.
	 * 
	 * @param coverage
	 */
	private boolean validateTaxonomicCoverage(Coverage coverage) {

		TaxonomicCoverage taxonomicCoverageFromRequest = coverage
				.getTaxonomicCoverage();

		// FIXME reset the taxonomic coverage - struts is passing badly
		// populated values
		TaxonomicCoverage taxonomicCoverage = new TaxonomicCoverage();
		taxonomicCoverage.setTaxonomicSystem(taxonomicCoverageFromRequest
				.getTaxonomicSystem());
		taxonomicCoverage.setScope(taxonomicCoverageFromRequest.getScope());
		coverage.setTaxonomicCoverage(taxonomicCoverage);

		if (classification != null && !classification.isEmpty()) {
			if (taxonomicCoverage == null) {
				taxonomicCoverage = new TaxonomicCoverage();
				coverage.setTaxonomicCoverage(taxonomicCoverage);
			}

			for (String singleClassification : classification) {
				log.debug("Processing classification: " + singleClassification);
				TaxonomicClassification tcl = new TaxonomicClassification();
				StringTokenizer st = new StringTokenizer(singleClassification,
						"|");
				boolean sciNameSupplied = false;
				boolean commonNameSupplied = false;
				// set rank
				if (st.hasMoreTokens()) {
					// set rank
					String rank = st.nextToken();
					log.debug("Rank value:" + rank);
					tcl.setTaxonRankName(rank);
				}
				// set taxon name
				if (st.hasMoreTokens()) {
					// set sci name
					String taxon = st.nextToken();
					if (StringUtils.isNotBlank(taxon))
						sciNameSupplied = true;
					log.debug("Taxon value:" + taxon);
					tcl.setTaxonRankValue(taxon);
				}
				// set common name
				if (st.hasMoreTokens()) {
					// set common name
					String commonName = st.nextToken();
					if (StringUtils.isNotBlank(commonName))
						commonNameSupplied = true;
					log.debug("Common Name value:" + commonName);
					tcl.setCommonName(commonName);
				}
				log.debug("Adding taxon: " + tcl);
				if (sciNameSupplied || commonNameSupplied) {
					taxonomicCoverage.addTaxonomicClassification(tcl);
				}
			}
		}

		if (taxonomicCoverage.sizeTaxonomicClassificationList() == 0) {
			addActionError(getText("eml.dataset.coverage.taxonomicCoverage.taxonomicClassification.mandatory"));
			return false;
		}
		return true;
	}

	/**
	 * Validate the Geographic Coverage.
	 * 
	 * @return
	 */
	private boolean validateGeographicCoverage(Coverage coverage) {

		GeographicCoverage gc = coverage.getGeographicCoverage();
		boolean geographicCoverageSupplied = false;

		if (gc != null) {

			if (gc.getBoundingCoordinates() != null) {
				BoundingCoordinates bc = gc.getBoundingCoordinates();

				if (bc.getEastBoundingCoordinate() != null
						&& bc.getWestBoundingCoordinate() != null
						&& bc.getNorthBoundingCoordinate() != null
						&& bc.getSouthBoundingCoordinate() != null) {
					geographicCoverageSupplied = true;
				} else {
					// required for JIBX marshalling - not pretty
					gc.setBoundingCoordinates(null);
				}
			}

			if (StringUtils.isNotEmpty(gc.getGeographicDescription())) {
				geographicCoverageSupplied = true;
			}

			if (gc.getDatasetGPolygon() != null) {
				DatasetGPolygon dgp = gc.getDatasetGPolygon();
				if (dgp.getDatasetGPolygonExclusionGRing() != null
						&& StringUtils.isNotEmpty(dgp
								.getDatasetGPolygonExclusionGRing().getGRing())) {
					geographicCoverageSupplied = true;
				} else {
					// required for JIBX marshalling - not pretty
					gc.setDatasetGPolygon(null);
				}
			}
		}

		if (!geographicCoverageSupplied) {
			addActionError(getText("eml.dataset.coverage.geographicCoverage.mandatory"));
		}
		return geographicCoverageSupplied;
	}

	/**
	 * View the metadata for this document.
	 * 
	 * @return
	 */
	public String viewMetadata() {
		log.debug("View metadata");
		this.eml = (Eml) getRequest().getSession().getAttribute("eml");
		this.currentDoc = (File) getRequest().getSession().getAttribute(
				currentFileSessionAttribute);
		return SUCCESS;
	}

	/**
	 * Write metadata to file
	 * 
	 * @return
	 */
	public String createMetadata() {
		log.debug("Writing metadata out to file");

		try {
			Eml sessionEml = getWorkingDocument();

			checkPurposeAndMaintenance(eml);

			currentDoc = (File) getRequest().getSession().getAttribute(
					"currentDoc");

			IBindingFactory bfact = BindingDirectory.getFactory(Eml.class);
			IMarshallingContext mctx = bfact.createMarshallingContext();

			if (currentDoc == null) {
				String docId = Long.toString(System.currentTimeMillis());
				this.currentDoc = org.gbif.metadata.io.MetadataFileUtils.getNewFile(context, docId);
				getRequest().getSession().setAttribute("currentDoc",
						this.currentDoc);

				// set the package Id
				sessionEml.setPackageId("org.gbif.eml." + docId);
				sessionEml.setSystem(getText("metadata.system"));
			}
			mctx.marshalDocument(sessionEml, "UTF-8", true,
					new FileOutputStream(this.currentDoc));
			return SUCCESS;

		} catch (Exception e) {
			log.error(e.getMessage(), e);
		}
		return ERROR;
	}

	/**
	 * Populate Purpose and Maintenance - required for Metacat compatibility.
	 * 
	 * @param eml
	 */
	public void checkPurposeAndMaintenance(Eml eml) {
		if (eml.getDataset().getPurpose() == null
				|| eml.getDataset().getPurpose().getPara() == null) {
			Purpose purpose = new Purpose();
			purpose.setPara("TODO");
			eml.getDataset().setPurpose(purpose);
		}

		if (eml.getDataset().getMaintenance() == null
				|| eml.getDataset().getMaintenance().getDescription() == null) {
			Maintenance m = new Maintenance();
			MaintenanceDescription d = new MaintenanceDescription();
			d.setPara("TODO");
			m.setDescription(d);
			eml.getDataset().setMaintenance(m);
		}
	}

	/**
	 * @return the eml
	 */
	public Eml getEml() {
		return eml;
	}

	/**
	 * @param eml
	 *            the eml to set
	 */
	public void setEml(Eml eml) {
		this.eml = eml;
	}

	/**
	 * @return the isoLanguageI18nCodeMap
	 */
	public Map<String, String> getIsoLanguageI18nCodeMap() {
		return this.isoLanguageI18nCodeMap;
	}

	/**
	 * @param isoLanguageI18nCodeMap
	 *            the isoLanguageI18nCodeMap to set
	 */
	public void setIsoLanguageI18nCodeMap(
			Map<String, String> isoLanguageI18nCodeMap) {
		this.isoLanguageI18nCodeMap = isoLanguageI18nCodeMap;
	}

	public void setServletContext(ServletContext context) {
		this.context = context;
	}

	/**
	 * @return the currentDoc
	 */
	public File getCurrentDoc() {
		return currentDoc;
	}

	/**
	 * @param currentDoc
	 *            the currentDoc to set
	 */
	public void setCurrentDoc(File currentDoc) {
		this.currentDoc = currentDoc;
	}

	/**
	 * @return the majorTaxonRanks
	 */
	public List<String> getMajorTaxonRanks() {
		return majorTaxonRanks;
	}

	/**
	 * @param majorTaxonRanks
	 *            the majorTaxonRanks to set
	 */
	public void setMajorTaxonRanks(List<String> majorTaxonRanks) {
		this.majorTaxonRanks = majorTaxonRanks;
	}

	/**
	 * @return the otherTaxonRanks
	 */
	public List<String> getOtherTaxonRanks() {
		return otherTaxonRanks;
	}

	/**
	 * @param otherTaxonRanks
	 *            the otherTaxonRanks to set
	 */
	public void setOtherTaxonRanks(List<String> otherTaxonRanks) {
		this.otherTaxonRanks = otherTaxonRanks;
	}

	/**
	 * @return the defaultDatasetLanguage
	 */
	public String getDefaultDatasetLanguage() {
		return defaultDatasetLanguage;
	}

	/**
	 * @param defaultDatasetLanguage
	 *            the defaultDatasetLanguage to set
	 */
	public void setDefaultDatasetLanguage(String defaultDatasetLanguage) {
		this.defaultDatasetLanguage = defaultDatasetLanguage;
	}

	/**
	 * @return the now
	 */
	public Date getNow() {
		return now;
	}

	/**
	 * @param now
	 *            the now to set
	 */
	public void setNow(Date now) {
		this.now = now;
	}

	/**
	 * @return the creatorRoles
	 */
	public List<String> getCreatorRoles() {
		return creatorRoles;
	}

	/**
	 * @param creatorRoles
	 *            the creatorRoles to set
	 */
	public void setCreatorRoles(List<String> creatorRoles) {
		this.creatorRoles = creatorRoles;
	}

	/**
	 * @return the classification
	 */
	public List<String> getClassification() {
		return classification;
	}

	/**
	 * @param classification
	 *            the classification to set
	 */
	public void setClassification(List<String> classification) {
		this.classification = classification;
	}

	/**
	 * @return the back
	 */
	public String getBack() {
		return back;
	}

	/**
	 * @param back
	 *            the back to set
	 */
	public void setBack(String back) {
		this.back = back;
	}

	public String getMetadataDirectoryPath() {
		return metadataDirectoryPath;
	}

	public void setMetadataDirectoryPath(String metadataDirectoryPath) {
		this.metadataDirectoryPath = metadataDirectoryPath;
	}

	public boolean isSingleDateEnabled() {
		return singleDateEnabled;
	}

	public void setSingleDateEnabled(boolean singleDateEnabled) {
		this.singleDateEnabled = singleDateEnabled;
	}	
}